╔══════════════════════════════════════════════════════════════════════════════╗ ║ NEURAL BATCH PROCESSOR — VOLLSTÄNDIGE TECHNISCHE DOKUMENTATION ║ ║ Multi-Provider Batch API: Claude + OpenAI + Mistral ║ ║ Flask Proxy + Vanilla JS Frontend · v4.0 ║ ╚══════════════════════════════════════════════════════════════════════════════╝ Diese Datei beschreibt alle Patterns, Endpoints und Code-Blöcke die nötig sind, um das Batch-API-System von Grund auf in einem neuen Projekt einzubauen. Kopierbereit für andere KI-Modelle als Implementation-Reference. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ INHALTSVERZEICHNIS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. ARCHITEKTUR & KONZEPT 2. FLASK PROXY SERVER (server.py) — Vollständige Erklärung 3. PROXY ENDPOINTS — Übersicht 4. PROVIDER: ANTHROPIC CLAUDE — Batch Submit + Poll + Results 5. PROVIDER: OPENAI — Batch Submit + Poll + Results 6. PROVIDER: MISTRAL — Batch Submit + Poll + Results 7. AUTO-POLL MECHANISMUS (Frontend JS) 8. EXPORT FUNKTIONEN (HTML / TXT / JSONL / Code-ZIP) 9. STANDALONE FILE PATTERN (api() Helper, serverUrl, localStorage) 10. WICHTIGE GOTCHAS & PROVIDER-UNTERSCHIEDE 11. ENVIRONMENT VARIABLES / SECRETS 12. MINIMALES SETUP FÜR NEUES PROJEKT 13. FULL COPY-PASTE: server.py ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. ARCHITEKTUR & KONZEPT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Browser (HTML/JS) │ │ fetch('/anthropic/v1/...') ← relative URL │ fetch('/openai/v1/...') │ fetch('/mistral/v1/...') │ ▼ Flask Server (server.py, Port 5000) ← API Keys liegen hier, sicher │ ├─► https://api.anthropic.com/v1/... ← mit x-api-key Header ├─► https://api.openai.com/v1/... ← mit Authorization: Bearer └─► https://api.mistral.ai/v1/... ← mit Authorization: Bearer WARUM PROXY: - API Keys bleiben server-seitig (nie im Browser-Code) - Ein Server bedient alle 3 Provider - CORS-Probleme durch Same-Origin vermieden - Standalone-Datei (lokal geöffnet) kann über konfigurierbaren serverUrl connecten BATCH API FLOW (alle Provider): 1. Submit → Server antwortet mit Batch-ID 2. Poll (alle 15s) → Status: processing → completed 3. Results laden → NDJSON (eine JSON-Zeile pro Request) 4. Export (HTML / TXT / JSONL / Code-ZIP) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2. FLASK PROXY SERVER — ERKLÄRUNG ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DEPENDENCIES (pip install): flask requests ENVIRONMENT VARIABLES (als Secrets setzen, NIE hardcoden): ANTHROPIC_API_KEY OPENAI_API_KEY MISTRAL_API_KEY KERNPRINZIP — proxy_to() Funktion: - Nimmt jede eingehende Request - Setzt API-Key im Header - Leitet 1:1 weiter (Method, Body, Query-Params) - Streamt Antwort zurück (chunk_size=8192) - Timeout: 300s (Batch-Result-Downloads können groß sein) CORS: - @app.after_request setzt Access-Control-Allow-Origin: * auf ALLE Responses - OPTIONS Preflight: eigene cors_preflight() Funktion → 204 No Content - WICHTIG: after_request reicht nicht für OPTIONS — explizites Handler nötig STATIC FILE SERVING: - Catch-All Route /* liefert statische Dateien aus CWD - Prefix-Check: api/, anthropic/, openai/, mistral/ → 404 statt Datei - Fallback: index.html (für SPA-Routing) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3. PROXY ENDPOINTS — ÜBERSICHT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /anthropic/ Catch-All → api.anthropic.com/ Header: x-api-key, anthropic-version: 2023-06-01 Forwarded: content-type, anthropic-beta /openai/ Catch-All → api.openai.com/ Header: Authorization: Bearer Forwarded: content-type /mistral/ Catch-All → api.mistral.ai/ Header: Authorization: Bearer Forwarded: content-type /api/openai/upload-batch POST raw JSONL body → Wrapper für OpenAI File Upload Erstellt multipart/form-data mit purpose=batch Gibt file-ID zurück: { "id": "file-xxxx" } /api/mistral/upload-batch POST raw JSONL body → Wrapper für Mistral File Upload (gleiche Logik wie OpenAI) /api/health GET → JSON Status aller 3 API Keys { status, anthropic_key_configured, openai_key_configured, mistral_key_configured, proxy_targets, version } /api/batch-results/ GET → Legacy Alias für Anthropic Batch Results NDJSON ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4. PROVIDER: ANTHROPIC CLAUDE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BATCH SUBMIT POST /anthropic/v1/messages/batches Body: { "requests": [ { "custom_id": "req-001", "params": { "model": "claude-sonnet-4-20250514", "max_tokens": 4096, "system": [ { "type": "text", "text": "You are a helpful assistant.", "cache_control": { "type": "ephemeral" } ← Prompt Caching (bis 90% Ersparnis) } ], "messages": [{ "role": "user", "content": "Hello!" }] } } ] } Response: { "id": "msgbatch_xxxx", "processing_status": "in_progress", ... } BATCH POLL GET /anthropic/v1/messages/batches/{batch_id} Response: { "id": "msgbatch_xxxx", "processing_status": "in_progress" | "ended", "request_counts": { "processing": 3, "succeded": 2, ← Tippfehler in der API — wirklich "succeded" nicht "succeeded" "errored": 0 } } STATUS DONE wenn: processing_status === "ended" BATCH RESULTS LADEN GET /anthropic/v1/messages/batches/{batch_id}/results Response: NDJSON (newline-delimited JSON), eine Zeile pro Request { "custom_id": "req-001", "result": { "type": "succeeded", "message": { "content": [{ "type": "text", "text": "..." }], "usage": { "input_tokens": 100, "output_tokens": 200 } } } } Bei Fehler: "result": { "type": "errored", "error": { "type": "...", "message": "..." } } MODELLE (aktuell): claude-sonnet-4-20250514 (Sonnet 4.6 — Standard) claude-opus-4-8 (Opus 4.8 — Stärkstes) claude-haiku-3-5 (Haiku 3.5 — Schnellstes/Günstigstes) BATCH ID PREFIX: msgbatch_ JS SUBMIT BEISPIEL: const r = await fetch('/anthropic/v1/messages/batches', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requests: prompts.map(({ id, text }) => ({ custom_id: id, params: { model: 'claude-sonnet-4-20250514', max_tokens: 4096, system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }], messages: [{ role: 'user', content: text }], }, })) }), }); const { id: batchId } = await r.json(); ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5. PROVIDER: OPENAI ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ OPENAI BATCH BRAUCHT ZUERST EINEN FILE-UPLOAD (2-Schritt-Prozess) SCHRITT 1 — JSONL DATEI HOCHLADEN: POST /api/openai/upload-batch ← unser Proxy-Wrapper (kein direkter OpenAI Endpoint) Body: raw NDJSON text (Content-Type: application/x-ndjson) Format jeder Zeile: { "custom_id": "req-001", "method": "POST", "url": "/v1/chat/completions", "body": { "model": "gpt-4.1", "max_tokens": 4096, "temperature": 0.7, "messages": [ { "role": "system", "content": "You are helpful." }, { "role": "user", "content": "Hello!" } ] } } Response: { "id": "file-xxxx", "object": "file", "purpose": "batch", ... } SCHRITT 2 — BATCH JOB ERSTELLEN: POST /openai/v1/batches Body: { "input_file_id": "file-xxxx", "endpoint": "/v1/chat/completions", "completion_window": "24h" } Response: { "id": "batch_xxxx", "status": "validating", ... } BATCH POLL: GET /openai/v1/batches/{batch_id} Response: { "id": "batch_xxxx", "status": "validating" | "in_progress" | "finalizing" | "completed" | "failed" | "expired" | "cancelling" | "cancelled", "request_counts": { "total": 10, "completed": 7, "failed": 0 }, "output_file_id": "file-yyyy" ← nur wenn completed } STATUS DONE wenn: status === "completed" && output_file_id vorhanden RESULTS LADEN: GET /openai/v1/files/{output_file_id}/content Response: NDJSON { "id": "batch_req_xxxx", "custom_id": "req-001", "response": { "status_code": 200, "body": { "choices": [{ "message": { "content": "..." } }], "usage": { "prompt_tokens": 100, "completion_tokens": 200 } } }, "error": null } Bei Fehler: "error": { "code": "...", "message": "..." }, "response": null MODELLE (aktuell): gpt-4.1 gpt-4.1-mini gpt-4o gpt-4o-mini o3 o4-mini BATCH ID PREFIX: batch_ JS SUBMIT BEISPIEL: // JSONL bauen const lines = requests.map(({ id, text }) => JSON.stringify({ custom_id: id, method: 'POST', url: '/v1/chat/completions', body: { model: 'gpt-4.1', max_tokens: 4096, temperature: 0.7, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: text }] }, })).join('\n'); // Upload const ur = await fetch('/api/openai/upload-batch', { method: 'POST', headers: { 'Content-Type': 'application/x-ndjson' }, body: lines, }); const { id: fileId } = await ur.json(); // Batch erstellen const br = await fetch('/openai/v1/batches', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input_file_id: fileId, endpoint: '/v1/chat/completions', completion_window: '24h' }), }); const { id: batchId } = await br.json(); ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6. PROVIDER: MISTRAL ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ MISTRAL BATCH IST INLINE — kein separater File-Upload nötig! Alle Requests direkt im Body des POST. BATCH SUBMIT: POST /mistral/v1/batch/jobs ← NICHT /v1/batches (wie OAI) — eigener Endpoint! Body: { "model": "codestral-latest", "endpoint": "/v1/chat/completions", "requests": [ { "custom_id": "req-001", "body": { "max_tokens": 4096, "temperature": 0.7, "messages": [ { "role": "system", "content": "You are helpful." }, { "role": "user", "content": "Hello!" } ] } } ] } ACHTUNG: "model" ist ein Top-Level-Feld, NICHT im "body" der einzelnen Requests! Response: { "id": "uuid-xxxx-...", "status": "QUEUED", ... } BATCH POLL: GET /mistral/v1/batch/jobs/{job_id} Response: { "id": "uuid-xxxx", "status": "QUEUED" | "RUNNING" | "SUCCESS" | "FAILED" | "TIMEOUT_EXCEEDED" | "CANCELLATION_REQUESTED" | "CANCELLED", "total_requests": 10, "succeeded_requests": 7, "failed_requests": 0, "output_file": "file-yyyy" ← NICHT "output_file_id"! (anders als OAI) } STATUS DONE wenn: status === "SUCCESS" && output_file vorhanden RESULTS LADEN: GET /mistral/v1/files/{file_id}/content Response: NDJSON (gleiches Format wie OpenAI Responses) MODELLE (aktuell): codestral-latest (Best für Code — meistens verwendet) mistral-large-latest mistral-small-latest open-codestral-mamba BATCH ID: UUID ohne Prefix (z.B. "a1b2c3d4-e5f6-...") JS SUBMIT BEISPIEL: const r = await fetch('/mistral/v1/batch/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'codestral-latest', endpoint: '/v1/chat/completions', requests: requests.map(({ id, text }) => ({ custom_id: id, body: { max_tokens: 4096, temperature: 0.7, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: text }], }, })), }), }); const { id: batchId } = await r.json(); ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 7. AUTO-POLL MECHANISMUS (Frontend JS) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WICHTIG: setInterval() NICHT verwenden — nutze rekursives setTimeout() (runPollCycle). Grund: setInterval() läuft weiter auch wenn ein Poll noch nicht fertig ist. Mit setTimeout kann der nächste Poll erst starten wenn der vorherige done ist. KOMPLETTES PATTERN: const POLL_INTERVAL_S = 15; let autoPollActive = false; let autoPollTimer = null; let pollCountdown = 0; let countdownTick = null; function startAutoPoll() { if (autoPollActive) return; autoPollActive = true; runPollCycle(); } function stopAutoPoll() { autoPollActive = false; clearTimeout(autoPollTimer); clearInterval(countdownTick); } async function runPollCycle() { if (!autoPollActive) return; const done = await doPoll(); // gibt true zurück wenn Batch fertig if (done) { stopAutoPoll(); return; } if (!autoPollActive) return; // Countdown im Button anzeigen pollCountdown = POLL_INTERVAL_S; countdownTick = setInterval(() => { pollCountdown--; btn.textContent = `Nächster Poll in ${pollCountdown}s`; if (pollCountdown <= 0) clearInterval(countdownTick); }, 1000); autoPollTimer = setTimeout(runPollCycle, POLL_INTERVAL_S * 1000); } async function doPoll() { const batchId = document.getElementById('activeBatchId').value.trim(); if (!batchId) return false; // Provider-Erkennung anhand ID-Prefix: if (batchId.startsWith('msgbatch_')) return await pollClaude(batchId); if (batchId.startsWith('batch_')) return await pollOpenAI(batchId); return await pollMistral(batchId); // Mistral: UUID ohne Prefix } // doPoll gibt true zurück → stopAutoPoll() wird aufgerufen → Ergebnisse laden ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8. EXPORT FUNKTIONEN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ UNIVERSELLE extractResult() FUNKTION (funktioniert für alle 3 Provider): function extractResult(r) { // Claude Format if (r.result !== undefined) { const ok = r.result?.type === 'succeeded'; return { id: r.custom_id || '?', content: ok ? (r.result?.message?.content?.[0]?.text || 'No content') : `ERROR: ${r.result?.error?.message || 'Unknown'}`, ok, }; } // OpenAI / Mistral Format if (r.response !== undefined) { const ok = (r.response?.status_code === 200) && !r.error; return { id: r.custom_id || '?', content: ok ? (r.response?.body?.choices?.[0]?.message?.content || 'No content') : `ERROR: ${r.error?.message || JSON.stringify(r.error)}`, ok, }; } return { id: r.custom_id || '?', content: JSON.stringify(r).slice(0, 120), ok: false }; } DOWNLOAD TRIGGER: function triggerDownload(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } EXPORT FORMATE: HTML → triggerDownload(buildHtmlExport(results, batchId), 'batch.html', 'text/html') TXT → triggerDownload(buildTxtExport(results, batchId), 'batch.txt', 'text/plain') JSONL → triggerDownload(results.map(r=>JSON.stringify(r)).join('\n'), 'batch.jsonl', 'application/x-ndjson') CODE-ZIP EXPORT (mit JSZip): CDN: // Regex für Code-Blöcke (backtick fenced, mit Sprach-Label) const CODE_BLOCK_RE = /```([\w+#.-]*)[ \t]*\r?\n([\s\S]*?)```/g; // Sprache → Dateiendung Map: const LANG_EXT = { html:'html', css:'css', js:'js', jsx:'jsx', ts:'ts', tsx:'tsx', python:'py', py:'py', java:'java', kotlin:'kt', swift:'swift', c:'c', cpp:'cpp', csharp:'cs', cs:'cs', go:'go', rust:'rs', ruby:'rb', php:'php', bash:'sh', shell:'sh', sql:'sql', json:'json', yaml:'yaml', yml:'yml', toml:'toml', xml:'xml', markdown:'md', vue:'vue', svelte:'svelte', dockerfile:'dockerfile', // Fallback: nutze Sprachname selbst als Extension }; async function exportCode(results) { const blocks = []; results.forEach(r => { const { id, content, ok } = extractResult(r); if (!ok) return; let match; CODE_BLOCK_RE.lastIndex = 0; let bi = 0; while ((match = CODE_BLOCK_RE.exec(content)) !== null) { const lang = (match[1] || 'txt').toLowerCase().trim() || 'txt'; const code = match[2]; if (!code.trim()) continue; const ext = LANG_EXT[lang] || lang || 'txt'; blocks.push({ lang, ext, code, filename: `${id.replace(/[^a-zA-Z0-9_-]/g,'_')}${bi>0?`_${bi}`:''}.${ext}`, folder: ext, }); bi++; } }); if (!blocks.length) { alert('Keine Code-Blöcke gefunden'); return; } if (blocks.length === 1) { triggerDownload(blocks[0].code, blocks[0].filename, 'text/plain'); return; } const zip = new JSZip(); blocks.forEach(b => zip.file(`${b.folder}/${b.filename}`, b.code)); // Manifest let manifest = `CODE MANIFEST\n${'='.repeat(50)}\n\n`; blocks.forEach((b, i) => { manifest += `[${i+1}] ${b.folder}/${b.filename} (${b.lang})\n`; }); zip.file('_manifest.txt', manifest); const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }); triggerDownload(blob, `code-export.zip`, 'application/zip'); } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9. STANDALONE FILE PATTERN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Problem: Eine HTML-Datei, die lokal (file://) geöffnet wird, kann keine relative URLs nutzen (kein Server). Lösung: konfigurierbarer serverUrl. PATTERN: const DEFAULT_SERVER = 'https://together-ai.replit.app'; let serverUrl = (localStorage.getItem('neural_server_url') || DEFAULT_SERVER).replace(/\/$/, ''); function api(path) { return serverUrl + path; } // Alle fetch-Calls nutzen api(): fetch(api('/anthropic/v1/messages/batches'), { ... }) fetch(api('/openai/v1/batches/' + batchId)) fetch(api('/api/health')) UI FÜR SERVER-URL: async function connectServer() { serverUrl = document.getElementById('serverUrlInput').value.trim().replace(/\/$/, ''); localStorage.setItem('neural_server_url', serverUrl); const r = await fetch(api('/api/health')); const d = await r.json(); // d.anthropic_key_configured, d.openai_key_configured, d.mistral_key_configured } INIT: document.addEventListener('DOMContentLoaded', () => { const saved = localStorage.getItem('neural_server_url') || DEFAULT_SERVER; serverUrl = saved.replace(/\/$/, ''); document.getElementById('serverUrlInput').value = saved; connectServer(); // Auto-connect beim Laden }); ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10. WICHTIGE GOTCHAS & PROVIDER-UNTERSCHIEDE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CLAUDE: ✓ Kein File-Upload nötig — alles inline im Submit-Body ✓ Prompt Caching: cache_control: { type: "ephemeral" } im system-Array → bis 90% günstiger ✗ anthropic-beta Header nötig für neue Features → anthropic-version: "2023-06-01" immer mitschicken → Batch ID Prefix: msgbatch_ → Status-Feld heißt "processing_status" (nicht "status") → Done-Condition: processing_status === "ended" → Results-URL: /v1/messages/batches/{id}/results (extra /results Pfad) OPENAI: ✓ 50% günstiger als Echtzeit ✗ 2-Schritt-Prozess: erst File Upload, dann Batch erstellen ✗ JSONL-Datei braucht purpose: "batch" beim Upload → Batch ID Prefix: batch_ → Status-Feld: "status" → Done-Condition: status === "completed" && output_file_id vorhanden → Results: GET /v1/files/{output_file_id}/content → result_counts.failed (nicht "errored" wie bei Claude) MISTRAL: ✓ Inline Batch wie Claude — kein File-Upload nötig ✓ 50% Rabatt · Codestral best für Code ✗ Endpoint: /v1/batch/jobs (nicht /v1/batches wie OAI!) ✗ model ist Top-Level-Feld im Job-Body — NICHT in den einzelnen request.body Objekten → Keine Batch ID Prefix — UUID (kein auto-detect möglich) → Status: QUEUED → RUNNING → SUCCESS (Großbuchstaben!) → Done-Condition: status === "SUCCESS" → Ergebnis-Feld: output_file (nicht output_file_id wie bei OAI!) → Results: GET /v1/files/{output_file}/content CORS: ! @app.after_request alleine reicht nicht — OPTIONS Requests brauchen eigenen Handler ! after_request Header werden bei OPTIONS Response überschrieben → Lösung: explizite cors_preflight() Funktion + OPTIONS in jedem Route-Decorator NDJSON PARSEN: const results = text.split('\n').filter(l => l.trim()).map(l => JSON.parse(l)); // Niemals JSON.parse() auf den ganzen Text — jede Zeile ist ein eigenes JSON-Objekt STREAMING vs BUFFERING: → Proxy nutzt stream=True + iter_content(8192) für alle Responses → Wichtig für große Result-Dateien (z.B. 1000 Batch-Ergebnisse) → Timeout 300s — Batch-Result-Downloads können lange dauern ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11. ENVIRONMENT VARIABLES / SECRETS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Auf Replit: Secrets-Tab (nie in .env oder Code hardcoden) Lokal: .env Datei (niemals committen) + python-dotenv NAMES: ANTHROPIC_API_KEY → https://console.anthropic.com/settings/keys OPENAI_API_KEY → https://platform.openai.com/api-keys MISTRAL_API_KEY → https://console.mistral.ai/api-keys ZUGRIFF IN PYTHON: import os key = os.environ.get("ANTHROPIC_API_KEY", "") if not key: return jsonify({"error": "ANTHROPIC_API_KEY not configured"}), 500 HEALTH CHECK GIBT NUR BOOLEAN ZURÜCK — nie den Key selbst: "anthropic_key_configured": bool(os.environ.get("ANTHROPIC_API_KEY", "")) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12. MINIMALES SETUP FÜR NEUES PROJEKT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SCHRITT 1 — Dependencies installieren: pip install flask requests SCHRITT 2 — server.py kopieren (siehe Abschnitt 13) SCHRITT 3 — Server URL: Deployed URL: https://together-ai.replit.app (Setzt auch den Standalone-Default in der HTML-Datei) SCHRITT 4 — Secrets setzen: ANTHROPIC_API_KEY=sk-ant-... OPENAI_API_KEY=sk-... MISTRAL_API_KEY=... SCHRITT 5 — Server starten: python3 server.py # Port 5000, oder PORT env var setzen SCHRITT 6 — Frontend nutzt relative URLs: fetch('/anthropic/v1/messages/batches', { method: 'POST', ... }) fetch('/openai/v1/batches/' + batchId) fetch('/mistral/v1/batch/jobs') fetch('/api/health') SCHRITT 7 (optional) — Standalone-Datei: Alle fetch()-Calls durch api(path) ersetzen wo api() = serverUrl + path Server-URL in localStorage speichern Connect-Button im UI MINIMALER PROJEKT-FILEBAUM: project/ ├── server.py ← Flask Proxy (alle 3 Provider) ├── index.html ← Frontend └── requirements.txt ← flask, requests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 13. FULL COPY-PASTE: server.py ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """ Anthropic + OpenAI + Mistral API Proxy Server v4.0 API keys live in environment — clients never see them. """ from flask import Flask, request, Response, send_from_directory, jsonify import requests import io import os app = Flask(__name__) ANTHROPIC_BASE = "https://api.anthropic.com" OPENAI_BASE = "https://api.openai.com" MISTRAL_BASE = "https://api.mistral.ai" ANT_VERSION = "2023-06-01" STREAM_CTS = ("text/event-stream", "application/x-ndjson", "application/octet-stream") def get_anthropic_key(): return os.environ.get("ANTHROPIC_API_KEY", "") def get_openai_key(): return os.environ.get("OPENAI_API_KEY", "") def get_mistral_key(): return os.environ.get("MISTRAL_API_KEY", "") # ── CORS ────────────────────────────────────────────── @app.after_request def add_cors_headers(response): response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS" response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, anthropic-beta, anthropic-version" return response def cors_preflight(): return Response("", status=204, headers={ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, anthropic-beta, anthropic-version", }) # ── PROXY CORE ──────────────────────────────────────── def proxy_to(base_url, headers, path): target = f"{base_url}/{path}" if request.query_string: target += "?" + request.query_string.decode() body = request.get_data() if request.method in ("POST", "PUT", "PATCH") else None try: r = requests.request( method=request.method, url=target, headers=headers, data=body, stream=True, timeout=300, ) ct = r.headers.get("Content-Type", "application/json") is_stream = any(s in ct for s in STREAM_CTS) def generate(): for chunk in r.iter_content(chunk_size=8192): if chunk: yield chunk return Response(generate() if is_stream else r.content, status=r.status_code, headers={"Content-Type": ct, "Access-Control-Allow-Origin": "*"}) except requests.Timeout: return jsonify({"error": "Request timed out"}), 504 except Exception as e: return jsonify({"error": str(e)}), 502 # ── ANTHROPIC ───────────────────────────────────────── def anthropic_headers(incoming): hdrs = {"x-api-key": get_anthropic_key(), "anthropic-version": ANT_VERSION} for h in ("content-type", "anthropic-beta"): v = incoming.get(h) if v: hdrs[h] = v return hdrs @app.route("/anthropic/", methods=["GET","POST","PUT","PATCH","DELETE","OPTIONS"]) def anthropic_proxy(path): if request.method == "OPTIONS": return cors_preflight() if not get_anthropic_key(): return jsonify({"error": "ANTHROPIC_API_KEY not configured"}), 500 return proxy_to(ANTHROPIC_BASE, anthropic_headers(request.headers), path) # ── OPENAI ──────────────────────────────────────────── def bearer_headers(key, incoming=None): hdrs = {"Authorization": f"Bearer {key}"} if incoming: ct = incoming.get("content-type") if ct: hdrs["content-type"] = ct return hdrs @app.route("/openai/", methods=["GET","POST","PUT","PATCH","DELETE","OPTIONS"]) def openai_proxy(path): if request.method == "OPTIONS": return cors_preflight() key = get_openai_key() if not key: return jsonify({"error": "OPENAI_API_KEY not configured"}), 500 return proxy_to(OPENAI_BASE, bearer_headers(key, request.headers), path) @app.route("/api/openai/upload-batch", methods=["POST"]) def openai_upload_batch(): key = get_openai_key() if not key: return jsonify({"error": "OPENAI_API_KEY not configured"}), 500 jsonl_bytes = request.get_data() if not jsonl_bytes: return jsonify({"error": "No JSONL data received"}), 400 r = requests.post( f"{OPENAI_BASE}/v1/files", headers={"Authorization": f"Bearer {key}"}, files={"file": ("batch_input.jsonl", io.BytesIO(jsonl_bytes), "application/x-ndjson")}, data={"purpose": "batch"}, timeout=120, ) return Response(r.content, status=r.status_code, headers={"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"}) # ── MISTRAL ─────────────────────────────────────────── @app.route("/mistral/", methods=["GET","POST","PUT","PATCH","DELETE","OPTIONS"]) def mistral_proxy(path): if request.method == "OPTIONS": return cors_preflight() key = get_mistral_key() if not key: return jsonify({"error": "MISTRAL_API_KEY not configured"}), 500 return proxy_to(MISTRAL_BASE, bearer_headers(key, request.headers), path) # ── HEALTH ──────────────────────────────────────────── @app.route("/api/health") def health(): ant, oai, mis = bool(get_anthropic_key()), bool(get_openai_key()), bool(get_mistral_key()) return jsonify({ "status": "ok" if (ant and oai and mis) else ("partial" if (ant or oai or mis) else "degraded"), "anthropic_key_configured": ant, "openai_key_configured": oai, "mistral_key_configured": mis, "version": "4.0.0", }) # ── STATIC FILES ────────────────────────────────────── @app.route("/", defaults={"path": "index.html"}) @app.route("/") def serve_static(path): if path.startswith(("api/", "anthropic/", "openai/", "mistral/")): return jsonify({"error": "Not found"}), 404 try: return send_from_directory(".", path) except Exception: return send_from_directory(".", "index.html") if __name__ == "__main__": port = int(os.environ.get("PORT", 5000)) print(f" Anthropic: {'✓' if get_anthropic_key() else '✗ MISSING'}") print(f" OpenAI: {'✓' if get_openai_key() else '✗ MISSING'}") print(f" Mistral: {'✓' if get_mistral_key() else '✗ MISSING'}") app.run(host="0.0.0.0", port=port, threaded=True) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ QUICK REFERENCE — ENDPOINT CHEATSHEET ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SUBMIT BATCH: Claude: POST /anthropic/v1/messages/batches → { id: "msgbatch_..." } OpenAI: POST /api/openai/upload-batch → POST /openai/v1/batches → { id: "batch_..." } Mistral: POST /mistral/v1/batch/jobs → { id: "uuid..." } POLL STATUS: Claude: GET /anthropic/v1/messages/batches/{id} → { processing_status } OpenAI: GET /openai/v1/batches/{id} → { status, output_file_id } Mistral: GET /mistral/v1/batch/jobs/{id} → { status, output_file } DONE WHEN: Claude: processing_status === "ended" OpenAI: status === "completed" && output_file_id Mistral: status === "SUCCESS" && output_file LOAD RESULTS: Claude: GET /anthropic/v1/messages/batches/{id}/results OpenAI: GET /openai/v1/files/{output_file_id}/content Mistral: GET /mistral/v1/files/{output_file}/content ALL RESULTS: NDJSON — split('\n'), filter(trim), map(JSON.parse) HEALTH: GET /api/health → { anthropic_key_configured, openai_key_configured, mistral_key_configured } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Ende der Dokumentation · Neural Batch Processor v4.0 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━